﻿using System.Collections.Generic;
using System.Linq;

namespace Innovative.ProXr
{
	/// <summary>
	/// Groups a set of RelayCommand objects into a single
	/// operation to be sent to the relay device. The response
	/// type can be specified using a IRelayByteConverter object.
	/// </summary>
	public class RelayCommandSet<R> : RelayCommandSet
	{
		private IResponseConverter<R> _responseConverter = null;

		/// <summary>
		/// Get/sets the response converter used to convert the RelayByte[]
		/// output of the relay controller response to a specific type.
		/// </summary>
		public IResponseConverter<R> ResponseConverter
		{
			get
			{
				return _responseConverter;
			}
			set
			{
				_responseConverter = value;
			}
		}

		public new R Send(RelayController relayDevice, RelayByte[] parameterValues = null)
		{
			R returnValue = default(R);

			if (this.ResponseConverter != null)
			{
				RelayByte[] response = base.Send(relayDevice, parameterValues);
				returnValue = this.ResponseConverter.Convert(response);
			}
			else
			{
				throw new NullConverterException();
			}

			return returnValue;
		}
	}

	/// <summary>
	/// 
	/// </summary>
	/// <typeparam name="R"></typeparam>
	/// <typeparam name="P1"></typeparam>
	public class RelayCommandSet<R, P1> : RelayCommandSet
	{
		private IResponseConverter<R> _responseConverter = null;
		private IParameterConverter<P1> _parameter1Converter = null;

		/// <summary>
		/// Get/sets the response converter used to convert the RelayByte[]
		/// output of the relay controller response to a specific type.
		/// </summary>
		public IResponseConverter<R> ResponseConverter
		{
			get
			{
				return _responseConverter;
			}
			set
			{
				_responseConverter = value;
			}
		}

		public IParameterConverter<P1> Parameter1Converter
		{
			get
			{
				return _parameter1Converter;
			}
			set
			{
				_parameter1Converter = value;
			}
		}

		public R Send(RelayController relayDevice, P1 parameter1Value = default(P1))
		{
			R returnValue = default(R);

			if (this.ResponseConverter != null && this.Parameter1Converter != null)
			{
				RelayByte[] input1 = this.Parameter1Converter.Convert(parameter1Value);
				RelayByte[] response = base.Send(relayDevice, input1);
				returnValue = this.ResponseConverter.Convert(response);
			}
			else
			{
				throw new NullConverterException();
			}

			return returnValue;
		}
	}

	/// <summary>
	/// 
	/// </summary>
	/// <typeparam name="R"></typeparam>
	/// <typeparam name="P1"></typeparam>
	/// <typeparam name="P2"></typeparam>
	public class RelayCommandSet<R, P1, P2> : RelayCommandSet
	{
		private IResponseConverter<R> _responseConverter = null;
		private IParameterConverter<P1> _parameter1Converter = null;
		private IParameterConverter<P2> _parameter2Converter = null;

		/// <summary>
		/// Get/sets the response converter used to convert the RelayByte[]
		/// output of the relay controller response to a specific type.
		/// </summary>
		public IResponseConverter<R> ResponseConverter
		{
			get
			{
				return _responseConverter;
			}
			set
			{
				_responseConverter = value;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		public IParameterConverter<P1> Parameter1Converter
		{
			get
			{
				return _parameter1Converter;
			}
			set
			{
				_parameter1Converter = value;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		public IParameterConverter<P2> Parameter2Converter
		{
			get
			{
				return _parameter2Converter;
			}
			set
			{
				_parameter2Converter = value;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="relayDevice"></param>
		/// <param name="parameter1Value"></param>
		/// <param name="parameter2Value"></param>
		/// <returns></returns>
		public R Send(RelayController relayDevice, P1 parameter1Value = default(P1), P2 parameter2Value = default(P2))
		{
			R returnValue = default(R);

			if (this.ResponseConverter != null && this.Parameter1Converter != null && this.Parameter2Converter != null)
			{
				RelayByte[] input1 = this.Parameter1Converter.Convert(parameter1Value);
				RelayByte[] input2 = this.Parameter2Converter.Convert(parameter2Value);
				RelayByte[] response = base.Send(relayDevice, input1.AppendBytes(input2));
				returnValue = this.ResponseConverter.Convert(response);
			}
			else
			{
				throw new NullConverterException();
			}

			return returnValue;
		}
	}

	public class RelayCommandSet<R, P1, P2, P3> : RelayCommandSet
	{
		private IResponseConverter<R> _responseConverter = null;
		private IParameterConverter<P1> _parameter1Converter = null;
		private IParameterConverter<P2> _parameter2Converter = null;
		private IParameterConverter<P3> _parameter3Converter = null;

		/// <summary>
		/// Get/sets the response converter used to convert the RelayByte[]
		/// output of the relay controller response to a specific type.
		/// </summary>
		public IResponseConverter<R> ResponseConverter
		{
			get
			{
				return _responseConverter;
			}
			set
			{
				_responseConverter = value;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		public IParameterConverter<P1> Parameter1Converter
		{
			get
			{
				return _parameter1Converter;
			}
			set
			{
				_parameter1Converter = value;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		public IParameterConverter<P2> Parameter2Converter
		{
			get
			{
				return _parameter2Converter;
			}
			set
			{
				_parameter2Converter = value;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		public IParameterConverter<P3> Parameter3Converter
		{
			get
			{
				return _parameter3Converter;
			}
			set
			{
				_parameter3Converter = value;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="relayDevice"></param>
		/// <param name="parameter1Value"></param>
		/// <param name="parameter2Value"></param>
		/// <returns></returns>
		public R Send(RelayController relayDevice, P1 parameter1Value = default(P1), P2 parameter2Value = default(P2), P3 parameter3Value = default(P3))
		{
			R returnValue = default(R);

			if (this.ResponseConverter != null && this.Parameter1Converter != null && this.Parameter2Converter != null && this.Parameter3Converter != null)
			{
				RelayByte[] input1 = this.Parameter1Converter.Convert(parameter1Value);
				RelayByte[] input2 = this.Parameter2Converter.Convert(parameter2Value);
				RelayByte[] input3 = this.Parameter3Converter.Convert(parameter3Value);

				RelayByte[] parameters = input1.AppendBytes(input2).AppendBytes(input3);

				RelayByte[] response = base.Send(relayDevice, parameters);
				returnValue = this.ResponseConverter.Convert(response);
			}
			else
			{
				throw new NullConverterException();
			}

			return returnValue;
		}
	}

	/// <summary>
	/// Groups a set of RelayCommand objects into a single
	/// operation to be sent to the relay device.
	/// </summary>
	public class RelayCommandSet
	{
		private RelayCommand[] _commands = null;
		private RelayByte[] _expectedResults = new RelayByte[0];

		/// <summary>
		/// Creates a new instance of the RelayCommandSet with default values.
		/// </summary>
		public RelayCommandSet()
		{
		}

		/// <summary>
		/// Gets/sets the command list for this command set.
		/// </summary>
		public RelayCommand[] Commands
		{
			get
			{
				return _commands.OrderBy(t => t.Ordinal).ToArray();
			}
			set
			{
				_commands = value;

				// ***
				// *** Throw an exception id the commands are not ordered correctly
				// ***
				this.VerifyCommands(_commands);
			}
		}

		/// <summary>
		/// The number of commands that are optional. This value when subtracted from the
		/// CommandCount value cannot be less than 2.
		/// </summary>
		public int OptionalCommandCount
		{
			get
			{
				return this.Commands.Where(t => !t.IsRequired).Count();
			}
		}

		/// <summary>
		/// Specifies the list of values that are expected as a result 
		/// of this operation.
		/// </summary>
		public RelayByte[] ExpectedResults
		{
			get
			{
				return _expectedResults;
			}
			set
			{
				_expectedResults = value;
			}
		}

		/// <summary>
		/// Sends this command to the device. The device must be configured
		/// using RelayDevice.Configuration before sending any command to
		/// to it.
		/// </summary>
		/// <param name="input">This parameter contains the optional commands
		/// required for this command. Note this does NOT include Command 1 (254)
		/// and the Command 2 (only Command 3 or more)
		/// </param>
		/// <returns>Returns the byte values returned by the device after the command completes</returns>
		public RelayByte[] Send(RelayController relayDevice, RelayByte[] parameterValues = null)
		{
			RelayByte[] returnValue = new RelayByte[1];

			// ***
			// *** The default return value is 0
			// ***
			returnValue[0] = 0;

			// ***
			// *** Validate the optional command values
			// ***
			if (this.ValidateOptionalCommands(parameterValues))
			{
				// ***
				// *** Create the command byte array
				// ***
				RelayByte[] commandData = this.CombineRelayCommands(parameterValues);

				// ***
				// *** Execute the command
				// ***
				returnValue = relayDevice.SendCommand(commandData);

				// ***
				// *** Check the return value(s) against the list
				// ***
				var qry = this.ExpectedResults.Where(t => t != null).Select(t => t.Value).Union(returnValue.Where(t => t != null).Select(t => t.Value));

				// ***
				// *** If there are not matches this indicates that
				// *** the return value was not expected.
				// ***
				if (qry.Count() == 0)
				{
					throw new UnexpectedResultException();
				}
			}

			return returnValue;
		}

		/// <summary>
		/// Takes the commands in this RelayCommandSet and combines their
		/// values into a single RelayByte[]. They are combined by sorting
		/// the RelayCommand.Ordinal property in ascending order.
		/// </summary>
		/// <returns>A RelayByte[] containing the combined Command properties of the RelayCommand objects</returns>
		internal RelayByte[] CombineRelayCommands(RelayByte[] parameterValues)
		{
			RelayByte[] returnValue = new RelayByte[0];

			// ***
			// *** Fill in each value, parameters must be in order
			// *** and the optional parameters are last
			// ***
			List<RelayByte> byteBuffer = new List<RelayByte>();
			int i = 0;

			foreach (RelayCommand command in this.Commands)
			{
				if (command.IsReadOnly)
				{
					// ***
					// *** Add the read-only value
					// ***
					byteBuffer.Add(command.Value);
				}
				else
				{
					if (i < parameterValues.Length)
					{
						byteBuffer.Add(parameterValues[i].Value);
						i++;
					}
					else
					{
						if (command.IsRequired)
						{
							throw new RequiredParameterMissingException(command.Name);
						}
					}
				}
			}

			// ***
			// *** Convert the List to an array
			// ***
			returnValue = byteBuffer.ToArray();

			return returnValue;
		}

		/// <summary>
		/// Matches the list of provided optional values against the
		/// expected list to ensure the values are within range.
		/// </summary>
		/// <param name="parameterValues">The actual optional values provided in the Send method.</param>
		/// <returns>Returns True if they match.</returns>
		internal bool ValidateOptionalCommands(RelayByte[] parameterValues)
		{
			bool returnValue = false;

			// ***
			// *** Calculate the minimum optional
			// *** command count. Every command requires
			// *** at least two commands.
			// ***
			int minimumCount = this.Commands.Count() - this.Commands.Where(t => t.IsReadOnly).Count() - this.OptionalCommandCount;

			if (parameterValues != null)
			{
				// ***
				// *** Make sure there are not to few or too many
				// ***
				if (parameterValues.Length >= minimumCount && parameterValues.Length <= this.Commands.Count())
				{
					// ***
					// *** Check the value of the command against the allowed values
					// ***
					int i = 0;

					foreach (RelayCommand command in this.Commands)
					{
						if (command.IsReadOnly)
						{
							if (!command.VerifyValue())
							{
								throw new UnexpectedParameterValueException(command.Value);
							}
						}
						else
						{
							if (!command.VerifyValue(parameterValues[i].Value))
							{
								throw new UnexpectedParameterValueException(parameterValues[i].Value);
							}

							i++;
						}
					}

					returnValue = true;
				}
				else
				{
					throw new ParameterValueMismatchException();
				}
			}
			else
			{
				// ***
				// *** Is it required?
				// ***
				if (minimumCount > 0)
				{
					throw new ParameterValueMismatchException();
				}
				else
				{
					returnValue = true;
				}
			}

			return returnValue;
		}

		/// <summary>
		/// Verifies that the commands are properly ordered. Command 254 must always
		/// be first and all optional parameter commands must be last.
		/// </summary>
		/// <param name="commands">A list of commands, in order, to verify.</param>
		internal void VerifyCommands(RelayCommand[] commands)
		{
			bool foundOptional = false;

			if (commands != null)
			{
				if (commands.First().Value == 254)
				{
					foreach (RelayCommand command in commands)
					{
						// ***
						// *** make sure all optional (IsRequired = false)
						// *** commands are at the end
						// ***
						if (!command.IsRequired)
						{
							foundOptional = true;
						}

						if (command.IsRequired && foundOptional)
						{
							throw new CommandOrderException();
						}
					}
				}
				else
				{
					throw new CommandOrderException();
				}
			}
		}
	}
}
